package Roteador;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import GUI.GUIShowStatus;
import IO.RoutersIO;
import Roteamento.Enlace;
import Roteamento.TabelaRoteamento;
import Roteamento.VetorRoteamento;


/**
 * Classe que representa um roteador.
 * @author David, Iury, Milena
 *
 */
public class Router {

	private final long DEZSEGUNDOS = 10000;
	private final long CINCOSEGUNDOS = 5000;
	private final long TRESSEGUNDOS = 3000;

	private long timeout = DEZSEGUNDOS; // Tempo de timeout
	private long lastSent; // ultima vez que notificou os vizinhos
	private List<Neighbour> neighbours; // Lista de vizinhos
	private String id;
	private InetAddress ip;
	private int port;
	private DatagramSocket socket; // Socket para a comunicacao deste roteador
	private long timeForSending = TRESSEGUNDOS;
	private TabelaRoteamento tabela;
	
	private GUIShowStatus tela;

	
	/**
	 * Cria um novo roteador
	 * @param id do roteador
	 * @param ip endereço da camada de rede do roteador
	 * @param port porta do socket rodando no roteador
	 * @throws Exception caso não consiga acessar a porta dada.
	 */
	public Router(String id, InetAddress ip, int port) throws Exception {
		neighbours = new LinkedList<Neighbour>();
		this.id = id;
		this.ip = ip;
		this.port = port;
		this.socket = new DatagramSocket(port);
		this.tela = new GUIShowStatus(this);
		this.tela.setVisible(true);
		this.start();
	}
	
	/**
	 * Lista os enlaces ligados ao roteador
	 * @return lista de enlaces.
	 */
	public Enlace[] getEnlaces(){
		Enlace[] result = new Enlace[neighbours.size()];
		for (int i = 0; i< neighbours.size(); i++){
			result[i] = new Enlace(id,neighbours.get(i).getId(),neighbours.get(i).getCost());
		}
		return result;
	}

	/**
	 * Adiciona um no na lista vizinhos
	 * @param n o nó à ser adicionado.
	 */
	public void addNode(Neighbour n){
		this.neighbours.add(n);
		imprimeDados("Adicionou no " + n);
		initTimerForNode(n);		
	}

	/**
	 * Este metodo envia dados para todos os vizinhos
	 * @throws Exception caso não consiga criar o datagrama.
	 */
	private synchronized void sendMessageToAllNeighbours() throws Exception {
		lastSent = System.currentTimeMillis();
		imprimeDados("Enviando dados para: ");
		if (neighbours.size()>0){
			for (Neighbour n: neighbours){
				if (n.isOn()){
					imprimeDados(n.toString() + "Conteudo: " + tabela.getDadosASeremEnviados().toString());
					byte[] sendData = new byte[1024];
					sendData = tabela.getDadosASeremEnviados().toString().getBytes();
					DatagramPacket p = new DatagramPacket(sendData,sendData.length,n.getIp(),n.getPort());
					socket.send(p);
				}
			}
		}
		else 
			imprimeDados("Ninguem conectado");
	}

	/**
	 * Inicia uma thread para cada no adicionado
	 * Ele olha se a ultima vez que o no enviou algo extrapola o timeout
	 * Se sim, o remove da lista de vizinhos.
	 * Verifica a cada 20 segundos
	 */
	private void initTimerForNode(final Neighbour n) {
		Timer t = new Timer(true);
		t.schedule(new TimerTask() {
			public void run() {
				if (System.currentTimeMillis() - n.getLastSent() > getTimeout() && n.isOn()){
					n.setOn(false);
					imprimeDados("O enlace para " + n + " caiu");
					tabela.removerVizinho(n.getId());
					atualizaTabela(tabela.getTabelaComoMatriz());
				}
			}
		},1,DEZSEGUNDOS);
	}

	/**
	 * Inicia um timer para enviar dados para os vizinhos
	 * Envia a cada tres segundos
	 */
	private void initTimerForSending() {
		Timer t = new Timer(true);
		t.schedule(new TimerTask() {
			public void run() {
				try {
					sendMessageToAllNeighbours();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		},1,getTimeForSending());
	}

	/**
	 *  Verifica se um no esta conectado
	 * @param ip do nó
	 * @param portSender
	 * @return o nó conectado ou null caso ele não esteja.
	 */
	private Neighbour getNode(InetAddress ip, int portSender) {
		Neighbour newNode = new Neighbour(ip,portSender);
		for (Neighbour n: neighbours){
			if (n.equals(newNode))
				return n;
		}
		return null;
	}

	
	/**
	 * Pega o socket do roteador
	 * @return socket udp
	 */
	private DatagramSocket getSocket(){
		return this.socket;
	}

	/**
	 * Muda o id do roteador
	 * @param id novo id do roteador
	 */
	public void setId(String id) {
		this.id = id;
	}

	/**
	 * Obtem o id atual do roteador
	 * @return id do roteador
	 */
	public String getId() {
		return id;
	}
	
	/**
	 * Inicializa o roteador colocando-o na rede
	 * @throws IOException caso não consiga colocar o roteador na rede.
	 */
	public void start() throws IOException{

		for(Neighbour n : RoutersIO.getNeighboursFromId(id)){
			addNode(n);
		}
		this.tabela = new TabelaRoteamento(id, getEnlaces());
		initTimerForSending();
		atualizaTabela(tabela.getTabelaComoMatriz());
		DatagramSocket socket = getSocket();

		byte[] receiveData = new byte[1024];
		
		VetorRoteamento recebido = null;

		while(true){
			//Recebe um pacote no socket
			DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
			socket.receive(receivePacket);
			
			 //Pega dados do emissor
			InetAddress ipSender = receivePacket.getAddress();
			int portSender = receivePacket.getPort();
			receiveData = receivePacket.getData();
			recebido = new VetorRoteamento(new String(receiveData));
			boolean alterou = tabela.update(recebido);
			if (alterou)
				try {
					sendMessageToAllNeighbours();
					imprimeDados("Atualizou Tabela");
					atualizaTabela(tabela.getTabelaComoMatriz());
				} catch (Exception e) {
					e.printStackTrace();
				}
			//Verifica se emissor está na lista de nós
			Neighbour n = getNode(ipSender,portSender);
			//Se sim, atualiza o momento do último envio deste, senão o adiciona
			if (n!= null){
				n.setLastSent(System.currentTimeMillis());
				n.setOn(true);
				imprimeDados("Recebeu " + new String(receiveData).trim() + " de " + n);
				tabela.adicionaNovoVizinho(n.getId(), n.getCost());
				atualizaTabela(tabela.getTabelaComoMatriz());
			}
		}

	}

	/**
	 * Imprime uma String na tela da interface gráfica
	 * @param msg mensagem a ser impressa.
	 */
	public void imprimeDados(String msg){
		tela.recebeDados(msg);
	}

	/**
	 * Atualiza a tabela de custo dos enlaces
	 * @param dados novos valores para a tabela
	 */
	public void atualizaTabela(String[][] dados){
		tela.atualizaTabela(dados);
	}

	/**
	 * Seta um novo timeout
	 * @param timeout novo timeout
	 */
	public void setTimeout(long timeout) {
		this.timeout = timeout;
	}

	/**
	 * Retorna o timeout
	 * @return timeout.
	 */
	public long getTimeout() {
		return timeout;
	}

	/**
	 * Seta o tempo para renviar um dado
	 * @param timeForSending tempo
	 */
	public void setTimeForSending(long timeForSending) {
		this.timeForSending = timeForSending;
	}

	/**
	 * Retorna o tempo para renviar um dado.
	 * @return tempo.
	 */
	public long getTimeForSending() {
		return timeForSending;
	}
}

